# 注意 本方法计算代价很高，只在有 GPU 的情况下才能尝试运行。它在 CPU 上是绝对难以运
# 行的。如果你无法在 GPU 上运行代码，那么就采用第一种方法。


# ---------------01 将 VGG16 卷积基实例化---------------
from keras.applications import VGG16

conv_base = VGG16(weights='imagenet', include_top=False, input_shape=(150, 150, 3))
# weights: None 代表随机初始化， 'imagenet' 代表加载在 ImageNet 上预训练的权值。
# include_top: 是否包括顶层的全连接层。
# input_shape: 可选，输入尺寸元组，仅当 include_top=False 时有效，否则输入形状必须是 (244, 244, 3)（对于 channels_last 数据格式），或者 (3, 244, 244)（对于 channels_first 数据格式）。它必须拥有 3 个输入通道，且宽高必须不小于 32。例如 (200, 200, 3) 是一个合法的输入尺寸。
# conv_base.summary()
# _________________________________________________________________
# Layer (type)                 Output Shape              Param #
# =================================================================
# input_1 (InputLayer)         (None, 150, 150, 3)       0
# _________________________________________________________________
# block1_conv1 (Conv2D)        (None, 150, 150, 64)      1792
# _________________________________________________________________
# block1_conv2 (Conv2D)        (None, 150, 150, 64)      36928
# _________________________________________________________________
# block1_pool (MaxPooling2D)   (None, 75, 75, 64)        0
# _________________________________________________________________
# block2_conv1 (Conv2D)        (None, 75, 75, 128)       73856
# _________________________________________________________________
# block2_conv2 (Conv2D)        (None, 75, 75, 128)       147584
# _________________________________________________________________
# block2_pool (MaxPooling2D)   (None, 37, 37, 128)       0
# _________________________________________________________________
# block3_conv1 (Conv2D)        (None, 37, 37, 256)       295168
# _________________________________________________________________
# block3_conv2 (Conv2D)        (None, 37, 37, 256)       590080
# _________________________________________________________________
# block3_conv3 (Conv2D)        (None, 37, 37, 256)       590080
# _________________________________________________________________
# block3_pool (MaxPooling2D)   (None, 18, 18, 256)       0
# _________________________________________________________________
# block4_conv1 (Conv2D)        (None, 18, 18, 512)       1180160
# _________________________________________________________________
# block4_conv2 (Conv2D)        (None, 18, 18, 512)       2359808
# _________________________________________________________________
# block4_conv3 (Conv2D)        (None, 18, 18, 512)       2359808
# _________________________________________________________________
# block4_pool (MaxPooling2D)   (None, 9, 9, 512)         0
# _________________________________________________________________
# block5_conv1 (Conv2D)        (None, 9, 9, 512)         2359808
# _________________________________________________________________
# block5_conv2 (Conv2D)        (None, 9, 9, 512)         2359808
# _________________________________________________________________
# block5_conv3 (Conv2D)        (None, 9, 9, 512)         2359808
# _________________________________________________________________
# block5_pool (MaxPooling2D)   (None, 4, 4, 512)         0
# =================================================================
# Total params: 14,714,688
# Trainable params: 14,714,688
# Non-trainable params: 0
# _________________________________________________________________


# 最后的特征图形状为 (4, 4, 512)。我们将在这个特征上添加一个密集连接分类器。
# 接下来，下一步有两种方法可供选择。
# 1、在你的数据集上运行卷积基，将输出保存成硬盘中的 Numpy 数组，然后用这个数据作
# 为输入，输入到独立的密集连接分类器中（与本书第一部分介绍的分类器类似）。这种
# 方法速度快，计算代价低，因为对于每个输入图像只需运行一次卷积基，而卷积基是目
# 前流程中计算代价最高的。但出于同样的原因，这种方法不允许你使用数据增强。
# 2、在顶部添加 Dense 层来扩展已有模型（即 conv_base），并在输入数据上端到端地运行
# 整个模型。这样你可以使用数据增强，因为每个输入图像进入模型时都会经过卷积基。
# 但出于同样的原因，这种方法的计算代价比第一种要高很多。


# ---------------使用数据增强的快速特征提取(在卷积基上添加一个密集连接分类器)---------------
# 模型的行为和层类似，所以你可以向 Sequential 模型中添加一个模型（比如 conv_base），
# 就像添加一个层一样。

from keras import models
from keras import layers

model = models.Sequential()
model.add(conv_base)
model.add(layers.Flatten())
model.add(layers.Dense(256, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
# model.summary()
# _________________________________________________________________
# Layer (type)                 Output Shape              Param #
# =================================================================
# vgg16 (Model)                (None, 4, 4, 512)         14714688
# _________________________________________________________________
# flatten_1 (Flatten)          (None, 8192)              0
# _________________________________________________________________
# dense_1 (Dense)              (None, 256)               2097408
# _________________________________________________________________
# dense_2 (Dense)              (None, 1)                 257
# =================================================================
# Total params: 16,812,353
# Trainable params: 16,812,353
# Non-trainable params: 0
# _________________________________________________________________
# 如你所见， VGG16 的卷积基有 14 714 688 个参数，非常多。在其上添加的分类器有 200 万
# 个参数。
# 在编译和训练模型之前，一定要“冻结”卷积基。 冻结（freeze）一个或多个层是指在训练
# 过程中保持其权重不变。如果不这么做，那么卷积基之前学到的表示将会在训练过程中被修改。
# 因为其上添加的 Dense 层是随机初始化的，所以非常大的权重更新将会在网络中传播，对之前
# 学到的表示造成很大破坏。
# 在 Keras 中，冻结网络的方法是将其 trainable 属性设为 False。
# print('This is the number of trainable weights '
#       'before freezing the conv base:', len(model.trainable_weights))

# -----------------------------------冻结模型-----------------------------
conv_base.trainable = False
# print('This is the number of trainable weights '
#       'after freezing the conv base:', len(model.trainable_weights))
# This is the number of trainable weights before freezing the conv base: 30
# This is the number of trainable weights after freezing the conv base: 4
# 如此设置之后，只有添加的两个 Dense 层的权重才会被训练。总共有 4 个权重张量，每层
# 2 个（主权重矩阵和偏置向量）。注意，为了让这些修改生效，你必须先编译模型。如果在编译
# 之后修改了权重的 trainable 属性，那么应该重新编译模型，否则这些修改将被忽略。
# 现在你可以开始训练模型了，使用和前一个例子相同的数据增强设置。

# ---------------准备数据---------------
import os

base_dir = '/home/python-test/py36-keras-demo01/cats_and_dogs_small'
train_dir = os.path.join(base_dir, 'train')
validation_dir = os.path.join(base_dir, 'validation')
test_dir = os.path.join(base_dir, 'test')

# ---------------利用冻结的卷积基端到端地训练模型---------------
from keras.preprocessing.image import ImageDataGenerator
from keras import optimizers

train_datagen = ImageDataGenerator(
    rescale=1. / 255,
    rotation_range=40,
    width_shift_range=0.2,
    height_shift_range=0.2,
    shear_range=0.2,
    zoom_range=0.2,
    horizontal_flip=True,
    fill_mode='nearest')
test_datagen = ImageDataGenerator(rescale=1. / 255)  # 注意，不能增强验证数据
train_generator = train_datagen.flow_from_directory(
    train_dir,  # 目标目录
    target_size=(150, 150),  # 将所有图像的大小调整为 150×150
    batch_size=20,
    class_mode='binary'  # 因为使用了binary_crossentropy损失，所以需要用二进制标签
)
validation_generator = test_datagen.flow_from_directory(
    validation_dir,
    target_size=(150, 150),
    batch_size=20,
    class_mode='binary'
)

# ---------------编译模型---------------
model.compile(loss='binary_crossentropy',
              optimizer=optimizers.RMSprop(lr=2e-5),
              metrics=['acc'])
# ---------------运行模型---------------

from keras.callbacks import ModelCheckpoint

filepath = "dog_vs_cat_vgg16_data_enhancement-{epoch:02d}-{val_acc:.4f}.hdf5"
# ModelCheckpoint保存模型
checkpoint = ModelCheckpoint(filepath, monitor='val_acc', verbose=1, save_best_only=True,
                             mode='max')
callbacks_list = [checkpoint]

history = model.fit_generator(
    train_generator,
    steps_per_epoch=100,
    epochs=30,
    validation_data=validation_generator,
    validation_steps=50,
    callbacks=callbacks_list
)
# ---------------绘制结果---------------
import matplotlib.pyplot as plt

acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']
epochs = range(1, len(acc) + 1)
plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()
plt.figure()
plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()
plt.show(block=False)
plt.show()
